﻿Imports Microsoft.VisualStudio.Text.Classification
Imports Microsoft.VisualStudio.Text
Imports Microsoft.VisualStudio.RegularExpression.Coloring

Namespace Microsoft.VisualStudio.RegularExpression.Parser
	''' <summary>
	''' Parses the regular expression into tokens.
	''' </summary>
    Friend NotInheritable Class Parser

        Friend Property Tokens As IList(Of Token)

        Private position As Integer

#Region "Language Constants"

        Private Const StartChar = "$"c
        Private Const EndChar = "^"c
        Private Const SpaceChar = " "c
        Private Const NewLineChar = ControlChars.Lf
        Private Const LineFeedChar = ControlChars.Cr
        Private Const TabChar = ControlChars.Tab
        Private Const StartCharGroupChar = "["c
        Private Const EndCharGroupChar = "]"c
        Private Const StartRepetitionsChar = "{"c
        Private Const EndRepetitionsChar = "}"c
        Private Const StartCaptureChar = "("c
        Private Const SecondCaptureChar = "?"c
        Private Const EndCaptureChar = ")"c
        Private Const EscapeChar = "\"c
        Private Const HexExpressionChar = "x"c
        Private Const UnicodeExpressionChar = "u"c
        Private Const OctalExpressionChar = "o"c
        Private Const OneOrManyChar = "+"c
        Private Const ZeroOrManyChar = "*"c
        Private Const ZeroOrOneChar = "?"c
        Private Const OrChar = "|"c
        Private Const NegativeCaptureModifierChar = "-"c
        Private Const CaptureSeparatorChar = ":"c
        Private Const NegativeChar = "!"c
        Private Const LessThanChar = "<"c
        Private Const EqualsChar = "="c
        Private Const NamedCaptureEnd = ">"c
        Private Const ValidExpressionChars = "dDwWsSAzZbBuxovtrnf"
        Private Const ValidCaptureModifiers = "imnsx"

#End Region

        Friend Sub Parse(ByVal text As String)
            Me.Tokens = New List(Of Token)
            Me.position = 0

            DropWhitespace(text)
            ParseStart(text)
            DropWhitespace(text)
            ParseBody(text, EndChar)
            DropWhitespace(text)
            ParseEnd(text)
        End Sub

        'Whitespace needs not be parsed for coloring. Ignore it when needed.
        Private Sub DropWhitespace(ByVal text As String)
            Do While position < text.Length AndAlso (text.Chars(position) = SpaceChar OrElse text.Chars(position) = NewLineChar OrElse text.Chars(position) = LineFeedChar OrElse text.Chars(position) = TabChar)
                position += 1
            Loop
        End Sub

        'Parse the body of the expression, looking for different language parts and defaulting to plain text
        Friend Sub ParseBody(ByVal text As String, ByVal [until] As Char)
            Do While position < text.Length AndAlso text.Chars(position) <> [until]
                If (Not ParseCharGroup(text)) AndAlso (Not ParseRepetition(text)) AndAlso (Not ParseExpression(text)) AndAlso (Not ParseEscapedExpression(text)) AndAlso (Not ParseMultiplier(text)) AndAlso (Not ParseCapture(text)) Then
                    position += 1
                End If
                DropWhitespace(text)
            Loop
        End Sub

        'Look for capture groups (of the form "(text)" in regular expressions)
        Friend Function ParseCapture(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = StartCaptureChar Then
                Dim firstChar = position
                position += 1
                'When the first character in the group is a "?", there are many options for modifiers
                If position < text.Length AndAlso text.Chars(position) = SecondCaptureChar Then
                    position += 1
                    ParseModifiedCapture(text)
                End If

                ParseBody(text, EndCaptureChar)
                If position < text.Length Then
                    If text.Chars(position) = EndCaptureChar Then
                        position += 1
                    End If
                    Tokens.Add(New Token(firstChar, position, TokenKind.Capture, text.Chars(position - 1) = EndCaptureChar))
                End If

                Return True
            End If
            Return False
        End Function

        'Look for special patterns in a capture group (of the form "(?capture)" in regular expressions).
        Private Sub ParseModifiedCapture(ByVal text As String)
            'Look for special modifiers to the capture. The modifiers are:
            ' i for ignore case
            ' m for multiline
            ' n for explicit capture
            ' s for single line
            ' s for ignore whitespace
            If position < text.Length AndAlso ValidCaptureModifiers.Contains(text.Chars(position).ToString()) Then
                position += 1
                If position < text.Length AndAlso text.Chars(position) = CaptureSeparatorChar Then
                    position += 1
                    Tokens.Add(New Token(position - 3, position, TokenKind.CaptureName))
                End If
                'Same as before, but prefixed with a '-' to turn off the setting
            ElseIf position < text.Length AndAlso text.Chars(position) = NegativeCaptureModifierChar Then
                position += 1
                If position < text.Length AndAlso ValidCaptureModifiers.Contains(text.Chars(position).ToString()) Then
                    position += 1
                    If position < text.Length AndAlso text.Chars(position) = CaptureSeparatorChar Then
                        position += 1
                        Tokens.Add(New Token(position - 4, position, TokenKind.CaptureName))
                    End If
                End If
                'Look for groups that aren't separate matches (of the form "(?:text)" in regular expressions)
            ElseIf position < text.Length AndAlso text.Chars(position) = CaptureSeparatorChar Then
                position += 1
                Tokens.Add(New Token(position - 2, position, TokenKind.CaptureName))
                'Look for lookahead expressions (of the form "(?=group)" in regular expressions)
            ElseIf position < text.Length AndAlso text.Chars(position) = EqualsChar Then
                position += 1
                Tokens.Add(New Token(position - 2, position, TokenKind.CaptureName))
                'Look for negative lookahead expressions (of the form "(?!group)" in regular expressions)
            ElseIf position < text.Length AndAlso text.Chars(position) = NegativeChar Then
                position += 1
                Tokens.Add(New Token(position - 2, position, TokenKind.CaptureName))
            ElseIf position < text.Length AndAlso text.Chars(position) = LessThanChar Then
                position += 1
                ParseNamedGroup(text)
                position += 1
                'Look for conditional expressions (of the form "(?(text)yes|no)" in regular expressions)
            ElseIf position < text.Length AndAlso text.Chars(position) = StartCaptureChar Then
                Dim firstChar = position - 2
                Do While position < text.Length AndAlso text.Chars(position) <> EndCaptureChar
                    position += 1
                Loop

                If position < text.Length Then
                    Tokens.Add(New Token(firstChar, position + 1, TokenKind.CaptureName, True))
                Else
                    Tokens.Add(New Token(firstChar, position, TokenKind.CaptureName))
                End If
                position += 1
            End If
        End Sub

        'Look for named groups (represented as "(?<name>text)" in regular expressions). Up to this point, the starting "(?<" will be parsed.
        'The remainder might be of the form "(?<=", "(?<!" or an actual named group.
        Private Sub ParseNamedGroup(ByVal text As String)
            'Look for lookbehind expressions (of the form "(?<=group)" in regular expressions)
            If position < text.Length AndAlso text.Chars(position) = EqualsChar Then
                Tokens.Add(New Token(position - 2, position + 1, TokenKind.CaptureName))
                'Look for negative lookbehind expressions (of the form "(?<!group)" in regular expressions)
            ElseIf position < text.Length AndAlso text.Chars(position) = NegativeChar Then
                Tokens.Add(New Token(position - 2, position + 1, TokenKind.CaptureName))
            Else
                'Look for named groups
                Dim firstChar = position - 2
                Do While position < text.Length AndAlso text.Chars(position) <> NamedCaptureEnd
                    position += 1
                Loop

                If position < text.Length Then
                    Tokens.Add(New Token(firstChar, position + 1, TokenKind.CaptureName, True))
                Else
                    Tokens.Add(New Token(firstChar, position, TokenKind.CaptureName))
                End If
            End If
        End Sub

        'Look for square brackets for character groups (represented as "[abcd]" in regular expressions)
        Friend Function ParseCharGroup(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = StartCharGroupChar Then
                Dim firstChar = position
                Do While position < text.Length AndAlso text.Chars(position) <> EndCharGroupChar
                    If text.Chars(position) = EscapeChar Then
                        position += 1
                    End If
                    If position < text.Length Then
                        position += 1
                    End If
                Loop
                If position < text.Length Then
                    Tokens.Add(New Token(firstChar, position + 1, TokenKind.CharGroup, True))
                Else
                    Tokens.Add(New Token(firstChar, position, TokenKind.CharGroup))
                End If

                position += 1
                Return True
            End If
            Return False
        End Function

        'Look for escaped expressions (represented as "\" followed by anything but this characters: dDwWsSAzZbBuxo)
        Friend Function ParseEscapedExpression(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = EscapeChar Then
                If position + 1 < text.Length Then
                    Tokens.Add(New Token(position, position + 1, TokenKind.EscapedExpression))
                    position += 2
                    Return True
                Else
                    Tokens.Add(New Token(position, position, TokenKind.EscapedExpression))
                    position += 1
                    Return True
                End If
            End If
            Return False
        End Function

        'Look for multipliers and separators ("|", "*", "+" and "?" in regular expressions)
        Friend Function ParseMultiplier(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = OneOrManyChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Multiplier))
                position += 1
                Return True
            ElseIf position < text.Length AndAlso text.Chars(position) = ZeroOrManyChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Multiplier))
                position += 1
                Return True
            ElseIf position < text.Length AndAlso text.Chars(position) = ZeroOrOneChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Multiplier))
                position += 1
                Return True
            ElseIf position < text.Length AndAlso text.Chars(position) = OrChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Multiplier))
                position += 1
                Return True
            End If
            Return False
        End Function

        'Look for escaped expressions (represented as "\" followed by any of this characters: dDwWsSAzZbBuxo)
        Friend Function ParseExpression(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = EscapeChar Then
                If position + 1 < text.Length AndAlso ValidExpressionChars.Contains(text.Chars(position + 1).ToString()) Then
                    Dim result As Integer
                    'Hexadecimal is represented by \x followed by two digits: \x00
                    If text.Chars(position + 1) = HexExpressionChar Then
                        If position + 3 < text.Length AndAlso Integer.TryParse(text.Substring(position + 2, 2), result) Then
                            Tokens.Add(New Token(position, position + 4, TokenKind.Expression))
                            position += 3
                            Return True
                        End If
                        'Unicode is represented by \u followed by four digits: \u0000
                    ElseIf text.Chars(position + 1) = UnicodeExpressionChar Then
                        If position + 5 < text.Length AndAlso Integer.TryParse(text.Substring(position + 2, 4), result) Then
                            Tokens.Add(New Token(position, position + 6, TokenKind.Expression))
                            position += 5
                            Return True
                        End If
                        'Octal is represented by \o followed by three digits: \o000
                    ElseIf text.Chars(position + 1) = OctalExpressionChar Then
                        If position + 4 < text.Length AndAlso Integer.TryParse(text.Substring(position + 2, 3), result) Then
                            Tokens.Add(New Token(position, position + 5, TokenKind.Expression))
                            position += 4
                            Return True
                        End If
                    End If
                    'All other expressions are of the form \x, where x is one of the valid characters
                    Tokens.Add(New Token(position, position + 2, TokenKind.Expression))
                    position += 2
                    Return True
                End If
            End If
            Return False
        End Function

        'Look for curly brackets for repetitions (represented as "{1}" in regular expressions)
        Friend Function ParseRepetition(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = StartRepetitionsChar Then
                Dim firstChar = position
                Do While position < text.Length AndAlso text.Chars(position) <> EndRepetitionsChar
                    position += 1
                Loop
                If position < text.Length Then
                    Tokens.Add(New Token(firstChar, position + 1, TokenKind.Repetition, True))
                Else
                    Tokens.Add(New Token(firstChar, position, TokenKind.Repetition))
                End If

                position += 1
                Return True
            End If
            Return False
        End Function

        'A regular expression may start with a "$" symbol
        Friend Function ParseStart(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = StartChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Delimiter))
                position += 1
                Return True
            End If
            Return False
        End Function

        'A regular expression may end with a "^" symbol
        Friend Function ParseEnd(ByVal text As String) As Boolean
            If position < text.Length AndAlso text.Chars(position) = EndChar Then
                Tokens.Add(New Token(position, position + 1, TokenKind.Delimiter))
                position += 1
                Return True
            End If
            Return False
        End Function
    End Class
End Namespace